// clang-format off
//
// Created by goksu on 4/6/19.
//

#include <algorithm>
#include <vector>
#include "rasterizer.hpp"
#include <opencv2/opencv.hpp>
#include <math.h>

bool SSAA = false;
bool MSAA = true;


rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    pos_buf.emplace(id, positions);

    return {id};
}

rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{
    auto id = get_next_id();
    ind_buf.emplace(id, indices);

    return {id};
}

rst::col_buf_id rst::rasterizer::load_colors(const std::vector<Eigen::Vector3f> &cols)
{
    auto id = get_next_id();
    col_buf.emplace(id, cols);

    return {id};
}

auto to_vec4(const Eigen::Vector3f& v3, float w = 1.0f)
{
    return Vector4f(v3.x(), v3.y(), v3.z(), w);
}


static bool insideTriangle(float x, float y, const Vector3f* _v)
{   
    // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
    int flag = -1;

    for(int i = 0; i < 3; ++ i){
        // current point
        Eigen::Vector3f p0 = {x, y, 0};
        // the 1st vertex
        Eigen::Vector3f p1 = _v[i];
        // the 2nd vertex
        Eigen::Vector3f p2 = _v[(i + 1) % 3];

        // the 1st vector (p1 - p0)
        Eigen::Vector3f v1 = p1 - p0;
        // the 2nd vector (p1 - p2)
        Eigen::Vector3f v2 = p1 - p2;

        // get cross product
        float cp = v1.cross(v2).z();
        if(cp == 0) continue;

        int sign = cp < 0 ? 0 : 1;

        if(flag == -1) flag = sign;

        if(sign != flag) return false;
    }

    return true;
}

static std::tuple<float, float, float> computeBarycentric2D(float x, float y, const Vector3f* v)
{
    float c1 = (x*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*y + v[1].x()*v[2].y() - v[2].x()*v[1].y()) / (v[0].x()*(v[1].y() - v[2].y()) + (v[2].x() - v[1].x())*v[0].y() + v[1].x()*v[2].y() - v[2].x()*v[1].y());
    float c2 = (x*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*y + v[2].x()*v[0].y() - v[0].x()*v[2].y()) / (v[1].x()*(v[2].y() - v[0].y()) + (v[0].x() - v[2].x())*v[1].y() + v[2].x()*v[0].y() - v[0].x()*v[2].y());
    float c3 = (x*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*y + v[0].x()*v[1].y() - v[1].x()*v[0].y()) / (v[2].x()*(v[0].y() - v[1].y()) + (v[1].x() - v[0].x())*v[2].y() + v[0].x()*v[1].y() - v[1].x()*v[0].y());
    return {c1,c2,c3};
}

void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
    auto& buf = pos_buf[pos_buffer.pos_id];
    auto& ind = ind_buf[ind_buffer.ind_id];
    auto& col = col_buf[col_buffer.col_id];

    float f1 = (50 - 0.1) / 2.0;
    float f2 = (50 + 0.1) / 2.0;

    Eigen::Matrix4f mvp = projection * view * model;
    for (auto& i : ind)
    {
        Triangle t;
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
        //Homogeneous division
        for (auto& vec : v) {
            vec /= vec.w();
        }
        //Viewport transformation
        for (auto & vert : v)
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
            vert.z() = vert.z() * f1 + f2;
        }

        for (int i = 0; i < 3; ++i)
        {
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
        }

        auto col_x = col[i[0]];
        auto col_y = col[i[1]];
        auto col_z = col[i[2]];

        t.setColor(0, col_x[0], col_x[1], col_x[2]);
        t.setColor(1, col_y[0], col_y[1], col_y[2]);
        t.setColor(2, col_z[0], col_z[1], col_z[2]);

        rasterize_triangle(t);
    }

    if(SSAA) {
        for(int x = 0; x < width; ++ x){
            for(int y = 0; y < height; ++ y){
                Eigen::Vector3f color(0, 0, 0);
                for(int i = 0; i < 4; ++ i){
                    color += frame_buf_2xssaa[get_index(x, y) + i];
                }
                color /= 4;
                set_pixel(Eigen::Vector3f(x, y, 1.0f), color);
            }
        }
    }
}

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();

    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle

    float min_x = width;
    float max_x = 0;
    float min_y = height;
    float max_y = 0;

    // Find out the bounding box of current triangle.
    for(int i = 0; i < 3; ++ i){
        min_x = std::min(min_x, v[i].x());
        max_x = std::max(max_x, v[i].x());
        min_y = std::min(min_y, v[i].y());
        max_y = std::max(max_y, v[i].y());
    }

    // iterate through the pixel
    for(int y = min_y; y < max_y; ++ y){
        for(int x = min_x; x < max_x; ++ x){

            if(MSAA) {
                float fineness = 0; //purity
                for(float j = 0.25; j < 1.0; j += 0.5){
                    for(float i = 0.25; i < 1.0; i += 0.5){
                        if(insideTriangle(x + j, y + i, t.v)) fineness += 0.25;
                    }
                }
                if(insideTriangle(x + 0.5, y + 0.5, t.v)){
                    auto[alpha, beta, gamma] = computeBarycentric2D(x + 0.5, y + 0.5, t.v);
                    float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                    float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                    z_interpolated *= w_reciprocal;

                    int index = get_index(x, y);
                    if(z_interpolated < depth_buf[index]){
                        Eigen::Vector3f p;
                        p << x, y, z_interpolated;

                        Eigen::Vector3f color = t.getColor() * fineness + (1 - fineness) * frame_buf[index];
                        set_pixel(p, color);
                        depth_buf[index] = z_interpolated;
                    }

                    if(fineness < 1) {
                        depth_buf[index] = std::numeric_limits<float>::infinity();
                    }

                    // if(z_interpolated < depth_buf[get_index(x, y)]){
                    //     Eigen::Vector3f p = {(float)x, (float)y, z_interpolated};
                    //     set_pixel(p, fineness * t.getColor());
                    //     depth_buf[get_index(x, y)] = z_interpolated;
                    // }
                }
            }
            else if(SSAA) {
                int index = 0;
                for(float i = 0.25; i < 1.0; i += 0.5){
                    for(float j = 0.25; j < 1.0; j += 0.5){
                        if(insideTriangle(x + i, y + j, t.v)){
                            auto[alpha, beta, gamma] = computeBarycentric2D(x + i, y + i, t.v);
                            float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                            float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                            z_interpolated *= w_reciprocal;
                            int index_ans = get_index(x, y) + index;
                            if(z_interpolated < depth_buf_2xssaa[index_ans]){
                                frame_buf_2xssaa[index_ans] = t.getColor();
                                depth_buf_2xssaa[index_ans] = z_interpolated;
                            }
                        }
                        index ++;
                    }
                }
            }
            else {
                // find if the current pixel is inside the triangle
                if(insideTriangle(x + 0.5, y + 0.5, t.v)){
                    // If so, use the following code to get the interpolated z value.
                    auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
                    float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
                    float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
                    z_interpolated *= w_reciprocal;

                    // set the current pixel (use the set_pixel function)
                    // to the color of the triangle (use getColor function)
                    // if it should be painted.
                    int index = get_index(x, y);
                    if(z_interpolated < depth_buf[index]){
                        depth_buf[index] = z_interpolated;
                        Eigen::Vector3f p;
                        p << x, y, z_interpolated;
                        set_pixel(p, t.getColor());
                    }
                }
            }
            
        }
    }

    

    // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
}

void rst::rasterizer::set_model(const Eigen::Matrix4f& m)
{
    model = m;
}

void rst::rasterizer::set_view(const Eigen::Matrix4f& v)
{
    view = v;
}

void rst::rasterizer::set_projection(const Eigen::Matrix4f& p)
{
    projection = p;
}

void rst::rasterizer::clear(rst::Buffers buff)
{
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
        std::fill(frame_buf_2xssaa.begin(), frame_buf_2xssaa.end(), Eigen::Vector3f{0, 0, 0});
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
        std::fill(depth_buf_2xssaa.begin(), depth_buf_2xssaa.end(), std::numeric_limits<float>::infinity());
    }
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h);

    frame_buf_2xssaa.resize(w * h * 4);
    depth_buf_2xssaa.resize(w * h * 4);
    
}

int rst::rasterizer::get_index(int x, int y)
{
    return (height-1-y)*width + x;
}

int rst::rasterizer::get_index_2xssaa(int x, int y)
{
    return (height * 2 - 1 - y)*width * 2 + x;
}

void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
    //old index: auto ind = point.y() + point.x() * width;
    auto ind = (height-1-point.y())*width + point.x();
    frame_buf[ind] = color;

}

// clang-format on